import java.io.Closeable;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.invoke.VarHandle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import sdl3.SDL_AudioSpec;
import static java.lang.foreign.MemorySegment.NULL;
import static java.lang.foreign.ValueLayout.JAVA_FLOAT;
import static sdl3.SDL_h.*;

// 基于SDL3音频核心接口,调用jextract生成的接口类
public final class Sdl3Audio implements Closeable {
	public static final boolean DEBUG_LOG = true;
	private static final int AUDIO_BUFFER_MIN_MS = 100; // 缓冲区下限(毫秒). 待播放的缓冲时长小于此值开始解码并填充,每次至少填充此时长
	private static final int AUDIO_BUFFER_MAX_MS = 120; // 缓冲区上限(毫秒). 每次最多填充此时长
	private static final int WORKER_LOOP_NS = 20_000_000; // 工作线程每次循环的时长(纳秒)
	private final @NotNull WorkerThread workerThread = new WorkerThread();
	private final @NotNull ReentrantLock lock = new ReentrantLock(); // for devices, closed
	private final @NotNull ArrayList<@NotNull Device> devices = new ArrayList<>();
	private boolean closed;

	static {
		if (!SDL_Init(SDL_INIT_AUDIO()))
			throw new RuntimeException("SDL_Init(AUDIO) failed: " + SDL_GetError().getString(0));
	}

	@Override
	public void close() {
		lock.lock();
		try {
			if (closed)
				return;
			closed = true;
		} finally {
			lock.unlock();
		}
		try {
			workerThread.join();
		} catch (final InterruptedException e) {
			//noinspection CallToPrintStackTrace
			e.printStackTrace();
		}
		for (final var device : devices) {
			device.close();
			device.destroy();
		}
		devices.clear();
	}

	public @NotNull Device createDevice(final int freq) {
		final var device = new Device(this, freq);
		lock.lock();
		try {
			if (!closed) {
				devices.add(device);
				if (!workerThread.isAlive())
					workerThread.start();
				return device;
			}
		} finally {
			lock.unlock();
		}
		device.destroy();
		throw new IllegalStateException("closed");
	}

	private final class WorkerThread extends Thread {
		private static final AtomicLong counter = new AtomicLong();

		private WorkerThread() {
			super("Sdl3AudioWorker-" + counter.incrementAndGet());
			//noinspection CallToPrintStackTrace
			setUncaughtExceptionHandler((_, e) -> e.printStackTrace());
			setDaemon(true);
			setPriority(MAX_PRIORITY);
		}

		@Override
		public void run() {
			var devices = new Device[2];
			var streams = new Stream[2];
			int deviceCount, streamCount = 0;
			for (final var deviceList = Sdl3Audio.this.devices; ; ) {
				final var timeBegin = System.nanoTime();
				lock.lock();
				try {
					if (closed)
						return;
					devices = deviceList.toArray(devices);
					deviceCount = deviceList.size();
				} finally {
					lock.unlock();
				}
				for (int i = 0; i < deviceCount; i++) {
					final var device = devices[i];
					final boolean closed;
					device.lock.lock();
					try {
						closed = device.closed;
						if (!closed) {
							streamCount = device.borrowedCount;
							if (streams.length < streamCount)
								streams = new Stream[streamCount];
							for (int j = 0; j < streamCount; j++)
								streams[j] = device.streams.get(j);
						}
					} finally {
						device.lock.unlock();
					}
					if (closed) {
						lock.lock();
						try {
							deviceList.remove(device);
						} finally {
							lock.unlock();
						}
						device.destroy();
					} else {
						final int frameSize = device.frameSize;
						final int bufMinFrames = device.bufMinFrames;
						final int bufMaxFrames = device.bufMaxFrames;
						for (int j = 0; j < streamCount; j++) {
							final float gainLeft, gainRight;
							final long beginFramePos, endFramePos, oldFramePos;
							final @Nullable Decoder decoder;
							final boolean autoReturn;
							final var stream = streams[j];
							stream.lock.lock();
							try {
								gainLeft = stream.gainLeft;
								gainRight = stream.gainRight;
								beginFramePos = stream.beginFramePos;
								endFramePos = stream.endFramePos;
								oldFramePos = stream.framePos;
								decoder = stream.decoder;
								autoReturn = stream.autoReturn;
								if (decoder != null && oldFramePos >= 0)
									stream.decoding = true;
							} finally {
								stream.lock.unlock();
							}
							if (decoder != null && oldFramePos >= 0) {
								var framePos = oldFramePos;
								if (SDL_GetAudioStreamQueued(stream.stream) < bufMinFrames * frameSize) {
									final int bufIdx = stream.curFrameBufIdx;
									final var buf = bufIdx == 0 ? stream.frameBuf0 : stream.frameBuf1;
									stream.curFrameBufIdx = bufIdx ^ 1;
									int newFrames = 0;
									for (; ; ) {
										int maxFrames = bufMaxFrames - newFrames;
										if (endFramePos >= 0 && framePos + maxFrames > endFramePos)
											maxFrames = Math.max((int)(endFramePos - framePos), 0);
										final int decodeFrames = decoder.decode(framePos, maxFrames, buf,
												newFrames * frameSize, gainLeft, gainRight);
										if (decodeFrames > 0) {
											framePos += decodeFrames;
											newFrames += decodeFrames;
											if (endFramePos >= 0 && framePos >= endFramePos)
												framePos = beginFramePos;
											if (newFrames >= bufMinFrames)
												break;
										} else
											framePos = framePos == beginFramePos ? -1 : beginFramePos; // 避免异常情况死循环,直接结束播放
										if (framePos < 0)
											break;
									}
									if (newFrames > 0) {
										SDL_PutAudioStreamDataNoCopy(stream.stream, buf, newFrames * frameSize, NULL, NULL);
										if (DEBUG_LOG)
											System.out.format("%d: Put %d%n", System.nanoTime(), newFrames);
									}
								}
								stream.lock.lock();
								try {
									if (stream.framePos == oldFramePos && oldFramePos != framePos && stream.decoder == decoder) // 状态改变时就忽略当前播放位置
										stream.framePos = framePos;
									else if (stream.decoder != decoder) {
										SDL_ClearAudioStream(stream.stream);
										decoder.close();
									}
									stream.decoding = false;
								} finally {
									stream.lock.unlock();
								}
							} else if (autoReturn && SDL_GetAudioStreamQueued(stream.stream) <= 0)
								device.returnStream(stream);
						}
					}
				}
				Arrays.fill(streams, null);
				Arrays.fill(devices, null);
				final var sleepNs = WORKER_LOOP_NS - (System.nanoTime() - timeBegin);
				if (sleepNs > 0)
					LockSupport.parkNanos(sleepNs);
			}
		}
	}

	public static final class Device implements Closeable {
		private final @NotNull Sdl3Audio audio;
		private final int freq;
		private final int deviceId;
		private final int frameSize;
		private final int bufMinFrames;
		private final int bufMaxFrames;
		private final @NotNull Arena arena = Arena.ofShared();
		private final @NotNull MemorySegment audioSpec = SDL_AudioSpec.allocate(arena);
		private final @NotNull ReentrantLock lock = new ReentrantLock(); // for streams, borrowedCount, gain, closed, paused
		private final @NotNull ArrayList<@NotNull Stream> streams = new ArrayList<>();
		private int borrowedCount;
		private float gain = 1;
		private boolean closed;
		private boolean paused;

		private Device(final @NotNull Sdl3Audio audio, final int freq) {
			this.audio = audio;
			this.freq = freq;
			deviceId = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK(), NULL);
			if (deviceId == 0)
				throw new UnsupportedOperationException("SDL_OpenAudioDevice failed: " + SDL_GetError().getString(0));
			frameSize = (int)JAVA_FLOAT.byteSize() * 2; // 2 channels
			bufMinFrames = Math.toIntExact((long)freq * AUDIO_BUFFER_MIN_MS / 1000);
			bufMaxFrames = Math.toIntExact((long)freq * AUDIO_BUFFER_MAX_MS / 1000);
			SDL_AudioSpec.format(audioSpec, SDL_AUDIO_F32()); // 内部只支持float精度的帧,适配现代的音频接口且处理损失小
			SDL_AudioSpec.channels(audioSpec, 2);
			SDL_AudioSpec.freq(audioSpec, freq);
		}

		@Override
		public void close() {
			lock.lock();
			try {
				closed = true;
			} finally {
				lock.unlock();
			}
		}

		private void destroy() {
			for (final var stream : streams)
				stream.destroy();
			streams.clear();
			SDL_CloseAudioDevice(deviceId);
			arena.close();
		}

		public int getFreq() {
			return freq;
		}

		public int getStreamCapacity() {
			VarHandle.acquireFence();
			return streams.size();
		}

		public int getStreamCount() {
			VarHandle.acquireFence();
			return borrowedCount;
		}

		public float getGain() {
			VarHandle.acquireFence();
			return gain;
		}

		public boolean isClosed() {
			VarHandle.acquireFence();
			return closed;
		}

		public boolean isPaused() {
			VarHandle.acquireFence();
			return paused;
		}

		public void setPaused(final boolean paused) {
			lock.lock();
			try {
				if (closed)
					throw new IllegalStateException("closed");
				if (this.paused != paused) {
					this.paused = paused;
					if (paused)
						SDL_PauseAudioDevice(deviceId);
					else
						SDL_ResumeAudioDevice(deviceId);
				}
			} finally {
				lock.unlock();
			}
		}

		public void setGain(final float gain) {
			lock.lock();
			try {
				if (closed)
					throw new IllegalStateException("closed");
				if (this.gain != gain) {
					this.gain = gain;
					SDL_SetAudioDeviceGain(deviceId, gain);
				}
			} finally {
				lock.unlock();
			}
		}

		/**
		 * @return 获取的Stream对象只能在returnStream及所属Device关闭之前访问
		 */
		public @NotNull Stream borrowStream() {
			final Stream stream;
			lock.lock();
			try {
				if (closed)
					throw new IllegalStateException("closed");
				final int n = borrowedCount;
				if (n < streams.size())
					stream = streams.get(n);
				else
					streams.add(stream = new Stream(this));
				borrowedCount = n + 1;
			} finally {
				lock.unlock();
			}
			return stream;
		}

		public void returnStream(final @NotNull Stream stream) {
			lock.lock();
			try {
				if (closed)
					throw new IllegalStateException("closed");
				for (int i = 0, e = borrowedCount - 1; i <= e; i++) {
					if (streams.get(i) == stream) {
						stream.reset();
						streams.set(i, streams.get(e));
						streams.set(e, stream);
						borrowedCount = e;
						return;
					}
				}
			} finally {
				lock.unlock();
			}
			throw new IllegalArgumentException("not borrowed from this device");
		}

		public void play(final @Nullable Sound sound) {
			//noinspection resource
			borrowStream().play(sound, true);
		}
	}

	public static final class Stream implements Closeable {
		private final @NotNull Device device;
		private final @NotNull MemorySegment stream;
		private final @NotNull MemorySegment frameBuf0;
		private final @NotNull MemorySegment frameBuf1;
		private final @NotNull ReentrantLock lock = new ReentrantLock(); // for beginFramePos, endFramePos, framePos, decoder, decoding, autoReturn
		private float gainLeft = 1;
		private float gainRight = 1;
		private long beginFramePos = -1;
		private long endFramePos = -1;
		private long framePos;
		private int curFrameBufIdx; // 只被Worker线程访问
		private @Nullable Decoder decoder;
		private boolean decoding;
		private boolean autoReturn;

		private Stream(final @NotNull Device device) {
			this.device = device;
			stream = SDL_CreateAudioStream(device.audioSpec, NULL);
			if (stream == NULL)
				throw new UnsupportedOperationException("SDL_CreateAudioStream failed: " + SDL_GetError().getString(0));
			if (!SDL_BindAudioStream(device.deviceId, stream)) {
				SDL_DestroyAudioStream(stream);
				throw new UnsupportedOperationException("SDL_BindAudioStream failed: " + SDL_GetError().getString(0));
			}
			final var bufSize = (long)device.bufMaxFrames * device.frameSize;
			frameBuf0 = device.arena.allocate(bufSize, 32);
			frameBuf1 = device.arena.allocate(bufSize, 32);
		}

		private void destroy() {
			SDL_DestroyAudioStream(stream);
		}

		@Override
		public void close() {
			device.returnStream(this);
		}

		public void reset() {
			stop();
			gainLeft = 1;
			gainRight = 1;
			beginFramePos = -1;
			endFramePos = -1;
			framePos = 0;
			VarHandle.releaseFence();
		}

		public float getGainLeft() {
			VarHandle.acquireFence();
			return gainLeft;
		}

		public float getGainRight() {
			VarHandle.acquireFence();
			return gainRight;
		}

		public void setGain(final float left, final float right) {
			gainLeft = left;
			gainRight = right;
			VarHandle.releaseFence();
		}

		public long getFramePos() {
			VarHandle.acquireFence();
			return framePos;
		}

		public double getSecondPos() {
			return (double)getFramePos() / device.freq;
		}

		public void setFramePos(final long framePos) {
			this.framePos = Math.max(framePos, 0);
			VarHandle.releaseFence();
		}

		public void setSecondPos(final double secondPos) {
			setFramePos((long)(secondPos * device.freq));
		}

		/**
		 * 播放超过endFramePos时回到beginFramePos继续播放
		 *
		 * @param beginFramePos <0表示不循环
		 * @param endFramePos   <0或超过总帧数会直到无法解码为止
		 */
		public void setLoopRange(final long beginFramePos, final long endFramePos) {
			lock.lock();
			try {
				this.beginFramePos = beginFramePos;
				this.endFramePos = endFramePos;
			} finally {
				lock.unlock();
			}
		}

		/**
		 * @param seconds from current to target, default:0
		 */
		public void setFadeSpeed(final float seconds) {
			//TODO
		}

		/**
		 * @param seconds from current to 0%, default:0
		 */
		public void setFadeOutSeconds(final float seconds) {
			//TODO
		}

		public void stop() {
			play(null, false);
		}

		public void play(final @Nullable Sound sound) {
			play(sound, false);
		}

		private void play(final @Nullable Sound sound, final boolean autoReturn) {
			if (sound != null && sound.sampleRate() != device.freq)
				throw new IllegalArgumentException("not matched freq: " + sound.sampleRate() + "!=" + device.freq);
			lock.lock();
			try {
				if (decoder != null && !decoding) {
					SDL_ClearAudioStream(stream);
					decoder.close();
				}
				if (sound != null) {
					if (framePos < 0)
						framePos = 0;
					decoder = sound.newDecoder();
					this.autoReturn = autoReturn;
				} else {
					framePos = 0;
					decoder = null;
					this.autoReturn = false;
				}
			} finally {
				lock.unlock();
			}
			if (sound != null)
				LockSupport.unpark(device.audio.workerThread); // 确保低延迟播放
		}

		public void playQueued(final @NotNull Sound sound) {
			//TODO
		}
	}

	/**
	 * 此类对象通常构造后就不会改变, 返回数据/字符串的方法必须返回固定不变的数据/字符串
	 */
	public interface Sound extends Closeable {
		int channels();

		int sampleRate();

		long totalFrames();

		default double totalSeconds() {
			return (double)totalFrames() / sampleRate();
		}

		@NotNull String name();

		@NotNull Decoder newDecoder();

		@Override
		default void close() {
		}
	}

	public interface Decoder extends Closeable {
		/**
		 * 此类对象不会被并发访问
		 *
		 * @param framePos  从多少帧开始解码(从0开始)
		 * @param maxFrames 最多可以写入的帧数. 可能传0,此时必须返回0
		 * @param dst       写入目标地址的基址. 必须写入float类型*2通道
		 * @param dstPos    写入目标地址的偏移(字节)
		 * @return 实际写入的帧数. 0表示已经无法再解出
		 */
		int decode(long framePos, int maxFrames, @NotNull MemorySegment dst, int dstPos, float gainLeft, float gainRight);

		@Override
		default void close() {
		}

		static void gainStereo(final @NotNull MemorySegment srcBuf, long srcPos, // frames*[float*1|2]
							   final @NotNull MemorySegment dstBuf, long dstPos, // frames*[float*2]
							   final int srcChannels, int frames, final float gainLeft, final float gainRight) {
			if (srcChannels == 1) {
				if (gainLeft == 1 && gainRight == 1) {
					for (; --frames >= 0; srcPos += 4, dstPos += 8) {
						final var a = srcBuf.get(JAVA_FLOAT, srcPos);
						dstBuf.set(JAVA_FLOAT, dstPos, a);
						dstBuf.set(JAVA_FLOAT, dstPos + 4, a);
					}
				} else {
					for (; --frames >= 0; srcPos += 4, dstPos += 8) {
						final var a = srcBuf.get(JAVA_FLOAT, srcPos);
						dstBuf.set(JAVA_FLOAT, dstPos, a * gainLeft);
						dstBuf.set(JAVA_FLOAT, dstPos + 4, a * gainRight);
					}
				}
			} else {
				if (gainLeft == 1 && gainRight == 1)
					MemorySegment.copy(srcBuf, srcPos, dstBuf, dstPos, frames * 8L);
				else {
					for (; --frames >= 0; srcPos += 8, dstPos += 8) {
						final var a = srcBuf.get(JAVA_FLOAT, srcPos);
						final var b = srcBuf.get(JAVA_FLOAT, srcPos + 4);
						dstBuf.set(JAVA_FLOAT, dstPos, a * gainLeft);
						dstBuf.set(JAVA_FLOAT, dstPos + 4, b * gainRight);
					}
				}
			}
		}
	}
}
